package com.googlecode.mylyn.core;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Set;

import org.apache.commons.lang.ObjectUtils;
import org.apache.commons.lang.StringUtils;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.mylyn.tasks.core.IRepositoryPerson;
import org.eclipse.mylyn.tasks.core.ITask.PriorityLevel;
import org.eclipse.mylyn.tasks.core.ITaskMapping;
import org.eclipse.mylyn.tasks.core.RepositoryResponse;
import org.eclipse.mylyn.tasks.core.RepositoryResponse.ResponseKind;
import org.eclipse.mylyn.tasks.core.TaskRepository;
import org.eclipse.mylyn.tasks.core.data.AbstractTaskDataHandler;
import org.eclipse.mylyn.tasks.core.data.TaskAttribute;
import org.eclipse.mylyn.tasks.core.data.TaskAttributeMapper;
import org.eclipse.mylyn.tasks.core.data.TaskAttributeMetaData;
import org.eclipse.mylyn.tasks.core.data.TaskCommentMapper;
import org.eclipse.mylyn.tasks.core.data.TaskData;

import com.google.gdata.data.BaseEntry;
import com.google.gdata.data.Content;
import com.google.gdata.data.DateTime;
import com.google.gdata.data.HtmlTextConstruct;
import com.google.gdata.data.Person;
import com.google.gdata.data.PlainTextConstruct;
import com.google.gdata.data.TextContent;
import com.google.gdata.data.projecthosting.BlockedOn;
import com.google.gdata.data.projecthosting.BlockedOnUpdate;
import com.google.gdata.data.projecthosting.Blocking;
import com.google.gdata.data.projecthosting.IssueCommentsEntry;
import com.google.gdata.data.projecthosting.IssueCommentsFeed;
import com.google.gdata.data.projecthosting.IssuesEntry;
import com.google.gdata.data.projecthosting.Label;
import com.google.gdata.data.projecthosting.Owner;
import com.google.gdata.data.projecthosting.OwnerUpdate;
import com.google.gdata.data.projecthosting.SendEmail;
import com.google.gdata.data.projecthosting.State;
import com.google.gdata.data.projecthosting.State.Value;
import com.google.gdata.data.projecthosting.Status;
import com.google.gdata.data.projecthosting.Summary;
import com.google.gdata.data.projecthosting.Updates;
import com.google.gdata.data.projecthosting.Username;
import com.googlecode.mylyn.core.client.IGoogleCodeClient;
import com.googlecode.mylyn.core.util.RepositoryUtils;

/**
 * This class is responsible for retrieving and posting task data to an Google
 * Code task repository.
 * 
 * @see TaskData
 */
class GoogleCodeTaskDataHandler extends AbstractTaskDataHandler {

    private static final String ATTRIBUTE_TYPE_DEFAULT = "Defect";

    private static final String ATTRIBUTE_STATUS_DEFAULT = "New";

    static final String GOOGLE_CODE_KEY = "googleCodeKey";

    private final GoogleCodeRepositoryConnector connector;

    /**
     * Initializes a {@link GoogleCodeTaskDataHandler}.
     * 
     * @param connector the Google Code connector to use
     */
    public GoogleCodeTaskDataHandler(GoogleCodeRepositoryConnector connector) {
        this.connector = connector;
    }

    @Override
    public TaskAttributeMapper getAttributeMapper(TaskRepository repository) {
        return new TaskAttributeMapper(repository);
    }

    @Override
    public boolean initializeTaskData(TaskRepository repository, TaskData data,
            ITaskMapping initializationData, IProgressMonitor monitor) throws CoreException {

        createAttribute(data, GoogleCodeAttribute.TASK_KEY);
        TaskAttribute statusAttribute = createAttribute(data, GoogleCodeAttribute.STATUS, ATTRIBUTE_STATUS_DEFAULT);
        LabelUtils.fillStatusOptions(statusAttribute);

        createAttribute(data, GoogleCodeAttribute.SUMMARY);
        
        if (data.isNew()) {
            createAttribute(data, GoogleCodeAttribute.DESCRIPTION_NEW);
        } else {
            createAttribute(data, GoogleCodeAttribute.DESCRIPTION_EXISTING);
        }

        TaskAttribute typeAttribute = createAttribute(data, GoogleCodeAttribute.TYPE, ATTRIBUTE_TYPE_DEFAULT);
        LabelUtils.fillTypeOptions(typeAttribute);

        TaskAttribute priority = createAttribute(data, GoogleCodeAttribute.PRIORITY);
        LabelUtils.fillPriorityOptions(priority);

        createAttribute(data, GoogleCodeAttribute.MILESTONE);

        createAttribute(data, GoogleCodeAttribute.DATE_MODIFICATION);
        createAttribute(data, GoogleCodeAttribute.DATE_CREATION);
        createAttribute(data, GoogleCodeAttribute.USER_ASSIGNED);

        createAttribute(data, GoogleCodeAttribute.URL);

        createAttribute(data, GoogleCodeAttribute.STARS);

        createAttribute(data, GoogleCodeAttribute.BLOCKED_ON);

        if (!data.isNew()) {
            createAttribute(data, GoogleCodeAttribute.COMMENT_NEW);
            createAttribute(data, GoogleCodeAttribute.USER_REPORTER);
            createAttribute(data, GoogleCodeAttribute.DATE_COMPLETION);
            createAttribute(data, GoogleCodeAttribute.BLOCKINGS);
        }

        return true;
    }
    
    private TaskAttribute createAttribute(TaskData data, GoogleCodeAttribute key,
            String attributeDefault) {
        TaskAttribute attribute = createAttribute(data, key);
        attribute.setValue(attributeDefault);
        return attribute;
    }

    private static TaskAttribute createAttribute(TaskData data, GoogleCodeAttribute key) {
        return createAttribute(data.getRoot(), key);
    }

    private static TaskAttribute createAttribute(TaskAttribute parent,
            GoogleCodeAttribute googleCodeAttribute) {
        TaskAttribute taskAttribute = parent.createAttribute(googleCodeAttribute.getKey());
        TaskAttributeMetaData metaData = taskAttribute.getMetaData();
        metaData.defaults();
        metaData.setReadOnly(googleCodeAttribute.isReadOnly());
        metaData.setKind(googleCodeAttribute.getKind());
        metaData.setLabel(googleCodeAttribute.getLabel());
        metaData.setType(googleCodeAttribute.getType());
        metaData.putValue(GOOGLE_CODE_KEY, googleCodeAttribute.getGoogleCodeKey());
        return taskAttribute;
    }

    @Override
    public RepositoryResponse postTaskData(TaskRepository repository, TaskData taskData,
            Set<TaskAttribute> oldAttributes, IProgressMonitor monitor) throws CoreException {

        RepositoryUtils.assertLoggedIn(repository);

        if (taskData.isNew()) {
            return insertIssue(repository, taskData, monitor);
        } else {
            return updateIssue(repository, taskData, oldAttributes, monitor);
        }
    }

    private RepositoryResponse insertIssue(TaskRepository repository, TaskData taskData,
            IProgressMonitor monitor) throws CoreException {

        IGoogleCodeClient client = getClient(repository);

        IssuesEntry entry = new IssuesEntry();
        entry.addLabel(new Label(getPriority(taskData)));
        String summary = getStringValue(taskData, GoogleCodeAttribute.SUMMARY);

        String description = getStringValue(taskData, GoogleCodeAttribute.DESCRIPTION_NEW);
        entry.getAuthors().add(client.getCurrentUser());

        String ownerName = getStringValue(taskData, GoogleCodeAttribute.USER_ASSIGNED);
        if (!StringUtils.isEmpty(ownerName)) {
            Owner owner = new Owner();
            owner.setUsername(new Username(ownerName));
            entry.setOwner(owner);
        }

        entry.setContent(new HtmlTextConstruct(description));
        entry.setTitle(new PlainTextConstruct(summary));
        entry.setStatus(new Status("New"));
        entry.setSendEmail(new SendEmail("False"));

        IssuesEntry created = client.createIssue(entry, monitor);
        String issueId = getIssueId(created);

        return new RepositoryResponse(ResponseKind.TASK_CREATED, issueId);
    }

    private RepositoryResponse updateIssue(TaskRepository repository, TaskData taskData,
            Set<TaskAttribute> oldAttributes, IProgressMonitor monitor) throws CoreException {

        IGoogleCodeClient client = getClient(repository);
        IssueCommentsEntry entry = new IssueCommentsEntry();
        Updates updates = new Updates();

        String summary = getStringValue(taskData, GoogleCodeAttribute.SUMMARY);
        updates.setSummary(new Summary(summary));

        String issueId = getIssueId(taskData);

        String comment = getNewComment(taskData);

        String currentPriority = getPriority(taskData);
        String oldPriority = getOldPriority(oldAttributes);
        if (!currentPriority.equals(oldPriority)) {
            if (oldPriority != null) {
                updates.addLabel(new Label('-' + oldPriority));
            }
            updates.addLabel(new Label(currentPriority));
        }

        String ownerName = getStringValue(taskData, GoogleCodeAttribute.USER_ASSIGNED);
        String oldOwner = getOldOwner(oldAttributes);
        if (!(ObjectUtils.equals(ownerName, oldOwner) || (StringUtils.isEmpty(ownerName) && StringUtils
                .isEmpty(oldOwner)))) {
            OwnerUpdate ownerUpdate;
            if (!StringUtils.isEmpty(ownerName)) {
                ownerUpdate = new OwnerUpdate(ownerName);
            } else {
                // remove the owner
                // FIXME broken, doesn't work, null doesn't work as well
                ownerUpdate = new OwnerUpdate(StringUtils.EMPTY);
            }
            updates.setOwnerUpdate(ownerUpdate);
        }

        updates.setStatus(new Status(getStringValue(taskData, GoogleCodeAttribute.STATUS)));

        computeBlockedOnDifference(taskData, oldAttributes, updates);

        entry.getAuthors().add(client.getCurrentUser());
        if (!StringUtils.isEmpty(comment)) {
            entry.setContent(new HtmlTextConstruct(comment));
        }
        entry.setSendEmail(new SendEmail("False"));
        entry.setUpdates(updates);
        client.updateIssue(issueId, entry, monitor);
        return new RepositoryResponse(ResponseKind.TASK_UPDATED, issueId);
    }

    private void computeBlockedOnDifference(TaskData taskData, Set<TaskAttribute> oldAttributes,
            Updates updates) {

        List<String> newBlockedOns = getNewBlockedOn(taskData);
        List<String> oldBlockedOns = getOldBlockedOn(taskData, oldAttributes, newBlockedOns);

        ArrayList<String> oldBlockOnsCopy = new ArrayList<String>(oldBlockedOns);
        oldBlockedOns.removeAll(newBlockedOns);
        newBlockedOns.removeAll(oldBlockOnsCopy);

        for (String oldBlockon : oldBlockedOns) {
            createBlockUpdate(oldBlockon, true, updates);
        }

        for (String newBlockon : newBlockedOns) {
            createBlockUpdate(newBlockon, false, updates);
        }
    }

    private List<String> getNewBlockedOn(TaskData taskData) {
        TaskAttribute blockedOnAttribute = getAttribute(taskData, GoogleCodeAttribute.BLOCKED_ON);
        List<String> newBlockedOns = taskData.getAttributeMapper().getValues(blockedOnAttribute);

        if (newBlockedOns != null && newBlockedOns.size() > 0) {
            String[] splittedIssueIds = newBlockedOns.get(0).split(",");
            newBlockedOns.clear();
            for (int i = 0; i < splittedIssueIds.length; i++) {
                String string = splittedIssueIds[i].trim();
                newBlockedOns.add(string);
            }
        } else {
            newBlockedOns = Collections.emptyList();
        }

        return newBlockedOns;
    }

    private List<String> getOldBlockedOn(TaskData taskData, Set<TaskAttribute> oldAttributes,
            List<String> newBlockedOns) {
        TaskAttribute oldBlockedOnAttribute = getOldAttribute(GoogleCodeAttribute.BLOCKED_ON,
                oldAttributes);

        List<String> oldBlockedOns = null;
        if (oldBlockedOnAttribute != null) {
            TaskAttributeMapper attributeMapper = taskData.getAttributeMapper();
            oldBlockedOns = attributeMapper.getValues(oldBlockedOnAttribute);
        } else {
            oldBlockedOns = Collections.emptyList();
        }

        return new ArrayList<String>(oldBlockedOns);
    }

    private void createBlockUpdate(String issueId, boolean remove, Updates updates) {
        if (!"".equals(issueId)) {
            String removeFlag = remove ? "-" : "";
            BlockedOnUpdate blockedOnUpdate = new BlockedOnUpdate();
            blockedOnUpdate.setValue(removeFlag + issueId);
            updates.addBlockedOnUpdate(blockedOnUpdate);
        }
    }

    private TaskAttribute getOldAttribute(GoogleCodeAttribute googleCodeAttribute,
            Set<TaskAttribute> oldAttributes) {
        for (TaskAttribute attribute : oldAttributes) {
            if (attribute.getId().equals(googleCodeAttribute.getKey())) {
                return attribute;
            }
        }
        return null;
    }

    private String getOldPriority(Set<TaskAttribute> oldAttributes) {
        TaskAttribute attribute = getOldAttribute(GoogleCodeAttribute.PRIORITY, oldAttributes);
        if (attribute != null) {
            return getPriority(attribute.getTaskData());
        } else {
            return null;
        }
    }

    private String getOldOwner(Set<TaskAttribute> oldAttributes) {
        TaskAttribute attribute = getOldAttribute(GoogleCodeAttribute.USER_ASSIGNED, oldAttributes);
        if (attribute != null) {
            return attribute.getValue();
        } else {
            return null;
        }
    }

    private String getPriority(TaskData taskData) {
        PriorityLevel level = PriorityLevel.fromString(getStringValue(taskData,
                GoogleCodeAttribute.PRIORITY));
        if (level != null) {
            String priority = LabelUtils.convertToGoogle(level);
            if (priority != null) {
                return "Priority-" + priority;
            }
        }
        return "Priority-" + LabelUtils.PRIORITY_MEDIUM;
    }

    private static String getNewComment(TaskData data) {
        return getStringValue(data, GoogleCodeAttribute.COMMENT_NEW);
    }

    private static String getIssueId(TaskData data) {
        return getStringValue(data, GoogleCodeAttribute.TASK_KEY);
    }

    private static String getStringValue(TaskData data, GoogleCodeAttribute attribute) {
        return data.getAttributeMapper().getValue(getAttribute(data, attribute));
    }

    private IGoogleCodeClient getClient(TaskRepository repository) throws CoreException {
        return connector.getClient(repository);
    }

    /**
     * Transfers the data from an Google Code specific {@link IssuesEntry} to a
     * Mylyn generic {@link TaskData}.
     * 
     * @param repository the repository to use
     * @param taskId the id of the task
     * @param issueEntry the issue entry
     * @param monitor the progress monitor
     * @return a mylyn task data object with the data from the Google Code issue
     * @throws CoreException if task data creation fails
     */
    public TaskData updateTaskData(TaskRepository repository, IssuesEntry issueEntry,
            IProgressMonitor monitor) throws CoreException {
        String repositoryUrl = repository.getRepositoryUrl();

        String issueId = getIssueId(issueEntry);
        TaskData data = new TaskData(getAttributeMapper(repository), repository.getConnectorKind(),
                repositoryUrl, issueId);
        this.initializeTaskData(repository, data, null, monitor);

        String title = issueEntry.getTitle().getPlainText();
        setAttributeValue(data, GoogleCodeAttribute.SUMMARY, title);
        setAttributeValue(data, GoogleCodeAttribute.USER_REPORTER, issueEntry.getAuthors().get(0).getName());
        
        String summary = getPlainTextContent(issueEntry, true);
        if (data.isNew()) {
            setAttributeValue(data, GoogleCodeAttribute.DESCRIPTION_NEW, summary);
        } else {
            setAttributeValue(data, GoogleCodeAttribute.DESCRIPTION_EXISTING, summary);
        }
        setAttributeValue(data, GoogleCodeAttribute.TASK_KEY, issueId);

        if (issueEntry.hasStatus()) {
            // {Status value=Fixed}{Status value=Duplicate}{Status
            // value=WontFix}{Status value=Accepted}
            Status status = issueEntry.getStatus();
            setAttributeValue(data, GoogleCodeAttribute.STATUS, status.getValue());
        }

        if (issueEntry.hasLabels()) {
            // [{Label value=Type-Defect}, {Label value=Priority-Medium}, {Label
            // value=Type-Enhancement}]
            for (Label label : issueEntry.getLabels()) {
                if (label.hasValue()) {
                    String value = label.getValue();
                    if (value.startsWith("Type-")) {
                        String type = value.substring("Type-".length());
                        setAttributeValue(data, GoogleCodeAttribute.TYPE, type);
                    } else if (value.startsWith("Priority-")) {
                        String priority = value.substring("Priority-".length());
                        PriorityLevel level = LabelUtils.convertToMylyn(priority);
                        if (level != null) {
                            setAttributeValue(data, GoogleCodeAttribute.PRIORITY, level.toString());
                        }
                    } else if (value.startsWith("Milestone-")) {
                        String milestone = value.substring("Milestone-".length());
                        setAttributeValue(data, GoogleCodeAttribute.MILESTONE, milestone);
                    }
                }
            }
        }

        setAttributeValue(data, GoogleCodeAttribute.DATE_CREATION, issueEntry.getPublished());
        setAttributeValue(data, GoogleCodeAttribute.DATE_MODIFICATION, issueEntry.getUpdated());

        if (issueEntry.hasOwner()) {
            IRepositoryPerson owner = getPerson(data, issueEntry.getOwner());
            setAttributeValue(data, GoogleCodeAttribute.USER_ASSIGNED, owner);
        }

        if (!data.isNew()) {
            TaskAttribute url = getAttribute(data, GoogleCodeAttribute.URL);
            url.setValue(this.connector.getTaskUrl(repository.getUrl(), issueId));
        }

        IGoogleCodeClient client = getClient(repository);
        IssueCommentsFeed comments = client.getAllComments(issueId, monitor);
        if (!data.isNew()) {
            addComments(issueId, data, comments, monitor);
        }

        // Set the completion date, this allows Mylyn mark the issue as
        // completed
        // There is no API for this so we make an educated guess
        State state = issueEntry.getState();
        if (!data.isNew() && Value.CLOSED == state.getValue()) {
            // find the last comment that set the issue status to the current
            // value
            // it would be better to find the last comment to set that state to
            // closed
            // but as long as we can't do that this will have to do

            // initialize with issue creation date in case we don't find any
            // comments
            DateTime closingDate = issueEntry.getPublished();
            for (IssueCommentsEntry comment : comments.getEntries()) {
                Updates updates = comment.getUpdates();
                if (updates != null && issueEntry.getStatus().equals(updates.getStatus())) {
                    closingDate = comment.getPublished();
                }
            }
            setAttributeValue(data, GoogleCodeAttribute.DATE_COMPLETION, closingDate);
        }

        data.getAttributeMapper().setIntegerValue(getAttribute(data, GoogleCodeAttribute.STARS),
                issueEntry.getStars().getValue());

        List<BlockedOn> blockedOn = issueEntry.getBlockedOns();
        List<String> rawBlockOns = new ArrayList<String>();
        for (BlockedOn blocking : blockedOn) {
            rawBlockOns.add(blocking.getId().getValue().toString());
        }
        setAttributeValue(data, GoogleCodeAttribute.BLOCKED_ON, rawBlockOns);

        List<Blocking> blockings = issueEntry.getBlockings();
        List<String> rawBlockings = new ArrayList<String>();
        for (Blocking blocking : blockings) {
            rawBlockings.add(blocking.getId().getValue().toString());
        }
        setAttributeValue(data, GoogleCodeAttribute.BLOCKINGS, rawBlockings);

        return data;
    }

    private String getIssueId(IssuesEntry issueEntry) {
        // id is something like
        // http://code.google.com/feeds/issues/p/googlecode-mylyn-connector/issues/full/1
        // and Mylyn doesn't like "-" in the id
        return StringUtils.substringAfterLast(issueEntry.getId(), "/");
    }

    private static String getPlainTextContent(BaseEntry<?> issueEntry, boolean stripHtml) {
        Content content = issueEntry.getContent();
        if (content instanceof TextContent) {
            // getTextContent() does the same thing and throws an
            // IllegalStateException
            // when the content is not TextContent
            TextContent textContent = (TextContent) content;
            if (textContent.getContent() != null
                    && textContent.getContent() instanceof HtmlTextConstruct) {
                HtmlTextConstruct html = (HtmlTextConstruct) textContent.getContent();
                String htmlContent = html.getHtml();
                if (stripHtml == true) {
                    return htmlContent.replaceAll("\\<.*?\\>", "");
                } else {
                    return htmlContent;
                }
            }
        }

        return StringUtils.EMPTY;
    }

    private void addComments(String issueId, TaskData data, IssueCommentsFeed comments,
            IProgressMonitor monitor) throws CoreException {

        int i = 1;
        for (IssueCommentsEntry issueComment : comments.getEntries()) {
            String content = getPlainTextContent(issueComment, false);
            if (!(StringUtils.isEmpty(content))) {
                TaskAttribute attribute = data.getRoot().createAttribute(
                        TaskAttribute.PREFIX_COMMENT + i);
                TaskCommentMapper taskComment = TaskCommentMapper.createFrom(attribute);
                // Comment id is something like
                // http://code.google.com/feeds/issues/p/googlecode-mylyn-connector/issues/1/comments/full/1
                String commentId = StringUtils.substringAfterLast(issueComment.getId(), "/");
                taskComment.setCommentId(commentId);
                List<Person> authors = issueComment.getAuthors();
                if (authors.size() == 1) {
                    IRepositoryPerson person = getPerson(data, authors.get(0));
                    taskComment.setAuthor(person);
                }
                taskComment.setNumber(i);
                taskComment.setText(content);
                taskComment.setCreationDate(new Date(issueComment.getPublished().getValue()));
                taskComment.setUrl(commentId);
                taskComment.applyTo(attribute);
            }
            i++;
        }
    }

    public static TaskAttribute getAttribute(TaskData taskData, GoogleCodeAttribute key) {
        return taskData.getRoot().getAttribute(key.getKey());
    }

    private static void setAttributeValue(TaskData data, GoogleCodeAttribute key, String value) {
        if (value != null) {
            TaskAttribute attribute = getAttribute(data, key);
            data.getAttributeMapper().setValue(attribute, value);
        }
    }

    private static void setAttributeValue(TaskData data, GoogleCodeAttribute key, DateTime dateTime) {
        if (dateTime != null) {
            TaskAttribute attribute = getAttribute(data, key);
            data.getAttributeMapper().setDateValue(attribute, new Date(dateTime.getValue()));
        }
    }

    private static void setAttributeValue(TaskData data, GoogleCodeAttribute key, List<String> list) {
        if (list != null) {
            TaskAttribute attribute = getAttribute(data, key);
            data.getAttributeMapper().setValues(attribute, list);
        }
    }

    private static void setAttributeValue(TaskData data, GoogleCodeAttribute key,
            IRepositoryPerson person) {
        if (person != null) {
            TaskAttribute attribute = getAttribute(data, key);
            setAttributeValue(data, attribute, person);
        }
    }

    private static void setAttributeValue(TaskData data, TaskAttribute attribute,
            IRepositoryPerson person) {
        if (person != null) {
            data.getAttributeMapper().setRepositoryPerson(attribute, person);
        }
    }

    private IRepositoryPerson getPerson(TaskData data, Person person) {
        TaskRepository repository = data.getAttributeMapper().getTaskRepository();
        String email = person.getEmail();
        IRepositoryPerson repositoryPerson;
        if (email != null) {
            repositoryPerson = repository.createPerson(email);
        } else {
            // seems to be the case in at least one case
            repositoryPerson = repository.createPerson(person.getName());
        }
        repositoryPerson.setName(person.getName());
        return repositoryPerson;
    }

    private IRepositoryPerson getPerson(TaskData data, Owner owner) {
        TaskRepository repository = data.getAttributeMapper().getTaskRepository();
        IRepositoryPerson person = repository.createPerson(owner.getUsername().getValue());
        return person;
    }

}
